/**
 * Copyright (c) 2010 deniskir@gmail.com. All rights reserved.
 * 
 * @version 1.9.3
 * @author Denis Kiriusin
 */

package com.dksoftware.combo;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;

public class ComboServlet extends HttpServlet {

    /**
     *
     */
    private static final long serialVersionUID = -708497924441068698L;

    // a new RequestDetails object will be created for every single thread
    private static ThreadLocal<RequestDetails> requestDetails = new ThreadLocal<RequestDetails>();

    // created once during initialisation, read-only (no synchronisation required)
    private static Properties properties = new Properties();

    // CSS, JS and themes directories
    private static File cssDir, jsDir, themesDir;
    private static boolean disableCompression = false;

    private static long CSSLastModified;
	private static long JavaScriptLastModified;
	
	private String prevMD5CSS = null;
	private String prevMD5Javascript = null;
	
    @Override
	public void init() throws ServletException {
		try {
			// load properties
			// TODO: may be we should read properties on every request (every 10-30 sec.)
			// TODO: may be load from classpath by unique name?
			loadProperties("/WEB-INF/config.properties");
			// TODO: (should we create them?) create directories
			createDirectories();
		} catch (Exception e) {
			throw new ServletException("Initialisation failed: " + e);
		}
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // send 501 error response code
        response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    	// 1. Identify request type css/javascript, collect all request info required into RequestDetails object
    	// 2. Get collection of files for commons, theme and explicitly specified via 'resources' URL parameter
    	// 3. Check if request or files were modified
    	// 4. Get files as bytes, minify, compress
    	// 5. Set response headers

    	try {
	    	// identify request type css/javascript, collect all request info 
    		// required into RequestDetails object
	    	requestDetails.set(getRequestDetails(request));

	    	// get collection of files for commons, theme and explicitly specified 
	    	// via 'resources' URL parameter
	    	final Collection<File> fileResources = Collections.unmodifiableCollection(getResources(requestDetails.get()));

	    	// get eTag and LastModified
	    	final long lastModified = CIOUtils.lastModified(fileResources);
	    	final String eTag = generateEtag(fileResources, lastModified);

	    	// check if request or files were modified and set cache headers properly
	    	if (HttpUtils.isRequestModified(request, eTag)) {
	    		setResponseHeaders(request, response, eTag, lastModified);
	    	} else {
	    		setConditionalResponseHeaders(request, response);
	    	}


	    	for (File f : fileResources) {
	    		System.out.println(f.getAbsolutePath());
	    	}
    	} catch (Exception e) {
    		response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
    	}
    }

    //TODO comment
    void setResponseHeaders(HttpServletRequest request, HttpServletResponse response, 
			String eTag, long lastModified) {
		String cacheProxy = null;
		if (request.getScheme().equalsIgnoreCase("https") || requestDetails.get().version > 0L) {
			// add s-maxage to fix Apache mod_cache bug 
			// @see http://www.gossamer-threads.com/lists/apache/dev/344665
			final String s_maxage = getProperties().getProperty("prop.header.s-maxage");
			cacheProxy = "public, s-maxage=" + s_maxage; 
		} else {
			cacheProxy = "private";
		}

		response.setContentType(requestDetails.get().type.getMime());
		response.setHeader(HttpHeaders.Etag.getName(), eTag);
		response.setCharacterEncoding("UTF-8");
		// set Expires header
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.YEAR, 1);
		response.setDateHeader(HttpHeaders.Expires.getName(), cal.getTimeInMillis());
		// set Cache-Control header (private - instructs proxies in the path not to cache the resource, 
		// browser will still cache)
		final String max_age = getProperties().getProperty("prop.header.max-age");
		response.setHeader(HttpHeaders.CacheControl.getName(), cacheProxy + ", max-age=" + max_age);
		// Last-Modified: Tue, 15 Nov 2012 12:45:26 GMT
		response.setDateHeader(HttpHeaders.LastModified.getName(), lastModified);
    }

    //TODO comment
    void setConditionalResponseHeaders(HttpServletRequest request, HttpServletResponse response) {
		response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
		response.setHeader(HttpHeaders.ContentLength.getName(), "0");
		// set 'Last-Modified' for clients that don't understand ETag headers
		response.setHeader(HttpHeaders.LastModified.getName(), 
				request.getHeader(HttpHeaders.IfModifiedSince.getName()));
	}

    /**
     * The method gets a collection of <tt>File</tt> objects and generates ETag string based on their absolute path
     * and lastModified value.
     * 
     * @param fileResources - a collection of files the method uses to generate ETag string
     * @param lastModified - lastModified
     * @return ETag String, never null
     */
    String generateEtag (final Collection<File> fileResources, final long lastModified) {
    	final StringBuilder filepaths = new StringBuilder(fileResources.toString().length() + 20 );
		for (final File file : fileResources) {
			filepaths.append(file.getAbsolutePath()).append(",");
		}
		return "\"" + DigestUtils.md5Hex(filepaths.toString() + lastModified + disableCompression) + "\"";
    }

    /**
     * The method parses query string, constructs and returns RequestDetails object.
     * 
     * @param request - HttpServletRequest
     * @return RequestDetails object
     * @throws IOException
     */
    RequestDetails getRequestDetails (HttpServletRequest request) throws IOException {
    	final Map<String, String> qsParams = parseQueryString(request);

    	String strType = qsParams.get("type");
    	String resources = qsParams.get("resources");
    	String theme = qsParams.get("theme");
    	RequestDetails.Type type = strType.equals("css") ? RequestDetails.Type.css : RequestDetails.Type.js;
    	long v = qsParams.get("v") == null ? 0L : Long.parseLong(qsParams.get("v"));
    	return new RequestDetails.Builder(type).resources(resources).theme(theme).version(v).build();
    }

    /**
     * The method returns ordered set of files specified by 'resources' URL parameter. 
     * Return set of resource files.
     *
     * @param resources - an query string comma separated parameters to parse
     * @return unmodifiable collection of resource files, never null
     * @throws UnsupportedEncodingException
     * @throws MalformedURLException
     */
    Collection<File> getResources(final RequestDetails requestDetails) throws MalformedURLException, UnsupportedEncodingException {
    	Set<File> fileSet = new LinkedHashSet<File>();
        if (requestDetails == null) {
            return fileSet;
        }
        // get common resources
        final File dir = requestDetails.type == RequestDetails.Type.css ? getCssDir() : getJsDir();
        fileSet.addAll(CIOUtils.listFiles(dir, Collections.singleton(requestDetails.extension), true));
        // TODO: get themes resources
        // linkedFileSet.addAll(CIOUtils.listFiles(themesDir, Collections.singleton(requestDetails.extension), true));
        // get explicitly set resources via URL
        if (requestDetails.resources != null) {
	        final String params[] = requestDetails.resources.split(",");
	        for (String param : params) {
	            String realPath = getServletContext().getRealPath(param);
	            if (realPath != null) {
	            	fileSet.add(CIOUtils.getLocalFile(realPath));
	            }
	        }
        }
        return Collections.unmodifiableCollection(fileSet);
    }

    /**
     * The method tries to load .properties file from Context, like /WebContent/WEB-INF.
     * 
     * @param propertiesPath - path to .properties file
     * @throws IOException
     */
    void loadProperties(final String propertiesPath) throws IOException {
    	synchronized (properties) {
	    	// load properties from /WEB-INF/config.properties
	    	properties = CIOUtils.loadPropertiesFromContext(getServletContext(), propertiesPath);
	    	// TODO: implement logic to load properties from classpath by filename
    	}
    }

    /**
     * The method reads .properties configuration file and creates all necessary directories for 
     * <tt>css</tt>, <tt>js</tt> and <tt>themes</tt>.
     * 
     * @throws IOException
     */
    void createDirectories() throws IOException {
    	synchronized (properties) {
	    	cssDir = new File(getServletContext().getRealPath(properties.getProperty("prop.css.dir")));
	    	jsDir = new File(getServletContext().getRealPath(properties.getProperty("prop.js.dir")));
	    	themesDir = new File(getServletContext().getRealPath(properties.getProperty("prop.themes.dir")));
	    	FileUtils.forceMkdir(getCssDir());
	    	FileUtils.forceMkdir(getJsDir());
	    	FileUtils.forceMkdir(getThemesDir());
    	}
    }

	/**
     * The method parses query string and returns a Map of key->value query string parameters.
     * 
     * @param request - the HttpServletRequest
     * @return a Map of key->value query string parameters
     */
    static Map<String, String> parseQueryString(HttpServletRequest request) {
    	final Map<String,String> qsParams = HttpUtils.getQueryStringParameters(request.getRequestURL().toString());
    	if (qsParams.isEmpty()) {
    		throw new IllegalArgumentException("Query string not specified");
		} else if (qsParams.get("type") == null 
				|| (!qsParams.get("type").equals("css") && !qsParams.get("type").equals("js"))) {
			throw new IllegalArgumentException("Query string 'type' parameter is not specified " +
					"or in incorrect format, must be 'css' or 'js'");
		}
    	return qsParams;
    }
    
    // Getters

    File getCssDir() {
		return cssDir;
	}

	File getJsDir() {
		return jsDir;
	}

    File getThemesDir() {
		return themesDir;
	}

	Properties getProperties() {
		return properties;
	}
	
	// Servlet info

	@Override
	public String getServletInfo() {
		return "Combinatorius servlet by Denis Kiriusin (deniskir@gmail.com)";
	}
}
